將後端 FastAPI API 和前端Streamlit介面成功連接起來,並能在前端查詢後端資料庫中的題目列表,作為 Mock 題庫的測試環節,另外,也要確保API架構與前端呼叫流程正確對應,避免資料傳遞錯誤。
# backend/models.py
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base
class Problem(Base):
__tablename__ = "problems"
id = Column(Integer, primary_key=True, index=True)
slug = Column(String, unique=True, index=True)
title = Column(String, index=True)
difficulty = Column(String) # Easy/Medium/Hard
topic = Column(String) # Array/DP/BFS...
is_active = Column(Boolean, default=True)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
name = Column(String)
level = Column(String, default="beginner")
is_active = Column(Boolean, default=True)
submissions = relationship("Submission", back_populates="user")
class Submission(Base):
__tablename__ = "submissions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
problem_id = Column(Integer, ForeignKey("problems.id"))
language = Column(String, default="python")
code = Column(Text) #使用者貼過來的程式碼
verdict = Column(String, default="pending")
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="submissions")
把 API 的輸入/輸出格式獨立集中管理,避免把資料庫實體直接暴露出去。
說明:
# backend/dto.py
from pydantic import BaseModel, ConfigDict
from typing import Optional, Literal
# Problems
class ProblemOut(BaseModel):
id: int
slug: str
title: str
difficulty: str
topic: str
model_config = ConfigDict(from_attributes=True)
class ProblemCreate(BaseModel):
slug: str
title: str
difficulty: Literal["Easy", "Medium", "Hard"]
topic: str
# 查詢參數 DTO(給 FastAPI Depends 使用)
class ProblemQuery(BaseModel):
difficulty: Optional[str] = None
topic: Optional[str] = None
q: Optional[str] = None
# Users
class UserOut(BaseModel):
id: int
email: str
name: str
level: str
model_config = ConfigDict(from_attributes=True)
class UserCreate(BaseModel):
email: str
name: str
# Submissions
class SubmissionCreate(BaseModel):
user_id: int
problem_id: int
language: str = "python"
code: str
class SubmissionOut(BaseModel):
id: int
user_id: int
problem_id: int
language: str
verdict: str
model_config = ConfigDict(from_attributes=True)
把 Day 1 的 GET/problems 擴充為可篩選版本,並加入 POST/problems 新增題目。
# backend/routers/problems.py
from fastapi import APIRouter, Depends, Query, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from ..database import SessionLocal
from .. import models
from ..dto import ProblemOut, ProblemCreate
router = APIRouter(prefix="/problems", tags=["problems"])
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
@router.get("", response_model=List[ProblemOut])
def list_problems(
difficulty: Optional[str] = Query(None),
topic: Optional[str] = Query(None),
q: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
qs = db.query(models.Problem).filter(models.Problem.is_active == True)
if difficulty:
qs = qs.filter(models.Problem.difficulty == difficulty)
if topic:
qs = qs.filter(models.Problem.topic == topic)
if q:
like = f"%{q}%"
qs = qs.filter(models.Problem.title.ilike(like))
return qs.order_by(models.Problem.id.asc()).all()
@router.post("", response_model=ProblemOut, status_code=status.HTTP_201_CREATED)
def create_problem(payload: ProblemCreate, db: Session = Depends(get_db)):
# slug 唯一性檢查
exists = db.query(models.Problem).filter(models.Problem.slug == payload.slug).first()
if exists:
raise HTTPException(409, "slug already exists")
obj = models.Problem(**payload.model_dump(), is_active=True)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
提供可用的建立使用者與紀錄提交API。
說明:
# backend/routers/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from ..database import SessionLocal
from .. import models
from ..dto import UserCreate, UserOut
router = APIRouter(prefix="/users", tags=["users"])
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def create_user(payload: UserCreate, db: Session = Depends(get_db)):
exists = db.query(models.User).filter(models.User.email == payload.email).first()
if exists:
raise HTTPException(409, "email already exists")
obj = models.User(email=payload.email, name=payload.name)
db.add(obj); db.commit(); db.refresh(obj)
return obj
@router.get("", response_model=List[UserOut])
def list_users(db: Session = Depends(get_db)):
return db.query(models.User).filter(models.User.is_active == True).all()
# backend/routers/submissions.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from typing import List
from ..database import SessionLocal
from .. import models
from ..dto import SubmissionCreate, SubmissionOut
router = APIRouter(prefix="/submissions", tags=["submissions"])
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
@router.post("", response_model=SubmissionOut, status_code=status.HTTP_201_CREATED)
def create_submission(payload: SubmissionCreate, db: Session = Depends(get_db)):
obj = models.Submission(**payload.model_dump())
db.add(obj); db.commit(); db.refresh(obj)
return obj
@router.get("", response_model=List[SubmissionOut])
def list_submissions(db: Session = Depends(get_db)):
return db.query(models.Submission).order_by(models.Submission.id.desc()).all()
CORS:前端要呼叫後端,若沒有CORS就會被瀏覽器阻擋。
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from .database import Base, engine
from .routers import problems
from .routers import users
from .routers import submissions
app = FastAPI(title="AI LeetCode Tutor API")
# 建表
Base.metadata.create_all(bind=engine)
# CORS(開發階段先全開)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/", include_in_schema=False)
def root():
return RedirectResponse(url="/docs")
@app.get("/health")
def health():
return {"status": "ok"}
# 路由
app.include_router(problems.router)
app.include_router(users.router)
app.include_router(submissions.router)
做完以上步驟,就可以開啟後端:
python -m uvicorn backend.app:app --reload
新增功能:
# frontend/app.py
import streamlit as st
import requests
import os
API_BASE = os.getenv("API_BASE", "http://127.0.0.1:8000")
st.set_page_config(page_title="AI LeetCode 助教", layout="wide")
st.title("AI LeetCode 助教(Day 2)")
with st.sidebar:
st.header("後端狀態")
try:
ok = requests.get(f"{API_BASE}/health", timeout=3).json()
st.success(f"API: {ok}")
except Exception as e:
st.error(f"API 連線失敗:{e}")
st.subheader("題目列表(可篩選)")
col1, col2, col3 = st.columns(3)
with col1:
difficulty = st.selectbox("難度", ["", "Easy", "Medium", "Hard"])
with col2:
topic = st.text_input("主題(Array, DP...)", "")
with col3:
q = st.text_input("關鍵字(標題搜尋)", "")
params = {}
if difficulty: params["difficulty"] = difficulty
if topic: params["topic"] = topic
if q: params["q"] = q
try:
res = requests.get(f"{API_BASE}/problems", params=params, timeout=8)
data = res.json()
st.caption(f"共 {len(data)} 題")
for p in data:
st.write(f"- [{p['difficulty']}] {p['title']} / topic: {p['topic']} / slug: `{p['slug']}`")
except Exception as e:
st.error(f"讀取題目失敗:{e}")
接者就可以啟動前端:
streamlit run app.py